using System;
using System.Collections.Generic;
using System.Linq;
using NHibernate;
using NHibernate.Criterion;
using NHibernate.Dialect;
using NHibernate.Dialect.Function;
using NHibernate.Engine;
using NHibernate.SqlCommand;
using Telerik.Web.Mvc;
using TelerikMvcGridCustomBindingHelper.DynamicQuery;
using TelerikMvcGridCustomBindingHelper.Mapper;
using TelerikMvcGridCustomBindingHelper.NHibernate.Aggregates;
using TelerikMvcGridCustomBindingHelper.Util;

namespace TelerikMvcGridCustomBindingHelper.NHibernate.DynamicQuery
{
    internal class QueryOverDynamicWhereClause<TEntity, TViewModel> : BaseDynamicWhereClause<TEntity, TViewModel, ICriterion>
        where TEntity : class
        where TViewModel : class
    {
        public QueryOverDynamicWhereClause() { }

        public QueryOverDynamicWhereClause(FilterDescriptor filterDescriptor, CaseInsensitive<TEntity> caseInsensitive, bool queryAcceptNullValuesWhenFiltering, ValueResolvers queryValueResolvers)
            : base(filterDescriptor, caseInsensitive, queryAcceptNullValuesWhenFiltering, queryValueResolvers) { }

        private IProjection _propertyProjection;
        private IProjection PropertyProjection
        {
            get
            {
                if (_propertyProjection != null)
                    return _propertyProjection;

                if (TargetProperty.IsNotNullOrWhiteSpace())
                {
                    _propertyProjection = global::NHibernate.Criterion.Projections.Property(TargetProperty);
                }
                else
                {
                    var customExpression = GridPropertyInfo.CustomExpression;

                    if (customExpression == null)
                        throw new InvalidOperationException("Target property or expression for filter descriptor could not be found.");

                    var sqlString = customExpression.ToSqlString();

                    _propertyProjection = new BetterSqlProjection(sqlString, ignoreCast: true);
                }

                return _propertyProjection;
            }
        }

        public void OrElse(QueryOverDynamicWhereClause<TEntity, TViewModel> anotherQueryOverDynamicWhereClause)
        {
            MergePredicateFrom(anotherQueryOverDynamicWhereClause, true);
        }

        public void AndAlso(QueryOverDynamicWhereClause<TEntity, TViewModel> anotherQueryOverDynamicWhereClause)
        {
            MergePredicateFrom(anotherQueryOverDynamicWhereClause, false);
        }

        protected override ICriterion OrElse(ICriterion left, ICriterion right)
        {
            if (left == null)
                return right;

            if (right == null)
                return left;

            return Restrictions.Or(left, right);
        }

        protected override ICriterion AndAlso(ICriterion left, ICriterion right)
        {
            if (left == null)
                return right;

            if (right == null)
                return left;

            return Restrictions.And(left, right);
        }

        public override bool HasFilters()
        {
            return Predicate != null && PropertyProjection != null;
        }

        protected override ICriterion BuildPredicateCaseSensitive(object value)
        {
            var prop = PropertyProjection;
            var filterOperator = InnerFilterDescriptor.Operator;

            if (value == null)
            {
                filterOperator = EnforceEqualityOperator();
            }

            switch (filterOperator)
            {
                case FilterOperator.IsEqualTo:
                    return value == null ? Restrictions.IsNull(prop) : Restrictions.Eq(prop, value);

                case FilterOperator.IsNotEqualTo:
                    return value == null ? Restrictions.IsNotNull(prop) : Restrictions.Not(Restrictions.Eq(prop, value));

                case FilterOperator.StartsWith:
                    return Restrictions.Like(prop, Convert.ToString(value), MatchMode.Start);

                case FilterOperator.EndsWith:
                    return Restrictions.Like(prop, Convert.ToString(value), MatchMode.End);

                case FilterOperator.Contains:
                    return Restrictions.Like(prop, Convert.ToString(value), MatchMode.Anywhere);

                case FilterOperator.DoesNotContain:
                    return Restrictions.Not(Restrictions.Like(prop, Convert.ToString(value), MatchMode.Anywhere));

                case FilterOperator.IsGreaterThan:
                    return Restrictions.Gt(prop, value);

                case FilterOperator.IsGreaterThanOrEqualTo:
                    return Restrictions.Ge(prop, value);

                case FilterOperator.IsLessThan:
                    return Restrictions.Lt(prop, value);

                case FilterOperator.IsLessThanOrEqualTo:
                    return Restrictions.Le(prop, value);

                // I dont kown how to implement this
                //case FilterOperator.IsContainedIn:
                default:
                    throw new NotSupportedException(string.Format("The method or expression \"{0}\" is not supported.", InnerFilterDescriptor.Operator));
            }
        }

        protected override ICriterion BuildPredicateCaseInsensitive(string value)
        {
            var prop = PropertyProjection;
            var filterOperator = InnerFilterDescriptor.Operator;

            if (value == null)
            {
                filterOperator = EnforceEqualityOperator();
            }

            switch (filterOperator)
            {
                case FilterOperator.IsEqualTo:
                    return new UpperInsensitiveLikeExpression(prop, value, MatchMode.Exact);

                case FilterOperator.IsNotEqualTo:
                    return Restrictions.Not(new UpperInsensitiveLikeExpression(prop, value, MatchMode.Exact));

                case FilterOperator.StartsWith:
                    return new UpperInsensitiveLikeExpression(prop, value, MatchMode.Start);

                case FilterOperator.EndsWith:
                    return new UpperInsensitiveLikeExpression(prop, value, MatchMode.End);

                case FilterOperator.Contains:
                    return new UpperInsensitiveLikeExpression(prop, value, MatchMode.Anywhere);

                case FilterOperator.DoesNotContain:
                    return Restrictions.Not(new UpperInsensitiveLikeExpression(prop, value, MatchMode.Anywhere));

                // I dont kown how to implement this
                //case FilterOperator.IsContainedIn:
                default:
                    throw new NotSupportedException(string.Format("The method or expression \"{0}\" is not supported.", InnerFilterDescriptor.Operator));
            }
        }

        protected override ICriterion BuildPredicateThatAcceptNullValues(INullSubstitute nullSubstitutes, bool useNotEquals)
        {
            if (nullSubstitutes != null)
            {
                IList<object> nullValues = new List<object>(nullSubstitutes.NullValues().Count);

                foreach (var nullValue in nullSubstitutes.NullValues())
                {
                    try
                    {
                        var changeType = Reflector.ChangeType(nullValue, GridPropertyInfo.PropertyType);
                        nullValues.Add(changeType);
                    }
                    // ReSharper disable EmptyGeneralCatchClause
                    catch { }
                    // ReSharper restore EmptyGeneralCatchClause
                }

                var caseInsensitive = GridPropertyInfo.PropertyType == typeof(string) && nullSubstitutes.IsCaseInsensitive;

                var predicate = nullValues
                    .Select(nullValue => BuildPredicateThatAcceptNullValues(null, nullValue, useNotEquals, caseInsensitive))
                    .Aggregate<ICriterion, ICriterion>(null, OrElse);

                return OrElse(Predicate, predicate);
            }

            return BuildPredicateThatAcceptNullValues(Predicate, useNotEquals: useNotEquals);
        }

        private ICriterion BuildPredicateThatAcceptNullValues(ICriterion predicate = null, object nullValue = null, bool useNotEquals = false, bool caseInsensitive = false)
        {
            var property = PropertyProjection;

            var nullCriterion = nullValue == null ? Restrictions.IsNull(property) : Restrictions.Eq(property, nullValue);
            var stringNullValue = nullValue as string;

            if (caseInsensitive && !string.IsNullOrWhiteSpace(stringNullValue))
            {
                nullCriterion = new UpperInsensitiveLikeExpression(property, stringNullValue, MatchMode.Exact);
            }

            if (useNotEquals)
            {
                nullCriterion = Restrictions.Not(nullCriterion);
            }

            return OrElse(predicate, nullCriterion);
        }

        protected override ICriterion BuildPredicateWithBooleanSubstitutes(IEnumerable<object> booleanSubstitutes)
        {
            Guard.If(ConvertedValue is bool == false)
                .Throw<ArgumentException>(string.Format("Property \"{0}.{1}\" was not of type boolean.", typeof(TEntity).Name, TargetProperty));

            var prop = PropertyProjection;

            switch (InnerFilterDescriptor.Operator)
            {
                case FilterOperator.IsEqualTo:
                    return GetEqualsBooleanSubstitutePredicate(booleanSubstitutes, prop);

                case FilterOperator.IsNotEqualTo:
                    return GetNotEqualsBooleanSubstitutePredicate(booleanSubstitutes, prop);

                default:
                    throw new NotSupportedException(string.Format("The method or expression \"{0}\" is not supported when the value is a boolean.", InnerFilterDescriptor.Operator));
            }
        }

        private static ICriterion GetEqualsBooleanSubstitutePredicate(IEnumerable<object> substitutes, IProjection prop)
        {
            return substitutes.Select(substitute => substitute == null ?
                Restrictions.IsNull(prop) : Restrictions.Eq(prop, substitute)).
                Aggregate<ICriterion, ICriterion>(null, (current, expression) =>
                    current == null ? expression : Restrictions.Or(current, expression));
        }

        private static ICriterion GetNotEqualsBooleanSubstitutePredicate(IEnumerable<object> substitutes, IProjection prop)
        {
            return substitutes.Select(substitute => substitute == null ?
                Restrictions.IsNotNull(prop) : Restrictions.Not(Restrictions.Eq(prop, substitute)))
                .Aggregate<ICriterion, ICriterion>(null, (current, expression) =>
                    current == null ? expression : Restrictions.Or(current, expression));
        }
    }

    internal class UpperInsensitiveLikeExpression : InsensitiveLikeExpression
    {
        private readonly IProjection _projection;
        private readonly string _value;

        public UpperInsensitiveLikeExpression(IProjection projection, string value, MatchMode matchMode)
            : base(projection, value, matchMode)
        {
            _projection = projection;
            _value = matchMode.ToMatchString(value);
        }

        public override SqlString ToSqlString(ICriteria criteria, ICriteriaQuery criteriaQuery, IDictionary<string, IFilter> enabledFilters)
        {
            var sqlBuilder = new SqlStringBuilder();
            var columnNames = CriterionUtil.GetColumnNames(null, _projection, criteriaQuery, criteria, enabledFilters);

            if (columnNames.Length != 1)
            {
                throw new HibernateException("insensitive like may only be used with single-column properties");
            }

            if (criteriaQuery.Factory.Dialect is PostgreSQLDialect)
            {
                sqlBuilder.Add(columnNames[0]);
                sqlBuilder.Add(" ilike ");
            }
            else
            {
                var upperFunction = criteriaQuery.Factory.Dialect.Functions.FirstOrDefault(pair => pair.Key.Equals("upper", StringComparison.OrdinalIgnoreCase));
                var upper = Equals(upperFunction, default(KeyValuePair<string, ISQLFunction>))
                    ? "upper"
                    : upperFunction.Value.ToString();

                sqlBuilder.Add(upper)
                    .Add("(")
                    .Add(columnNames[0])
                    .Add(")")
                    .Add(" like ");
            }

            sqlBuilder.Add(criteriaQuery.NewQueryParameter(GetParameterTypedValue(criteria, criteriaQuery)).Single());

            return sqlBuilder.ToSqlString();
        }

        public override TypedValue[] GetTypedValues(ICriteria criteria, ICriteriaQuery criteriaQuery)
        {
            var typedValues = new List<TypedValue>();

            if (_projection != null)
            {
                typedValues.AddRange(_projection.GetTypedValues(criteria, criteriaQuery));
            }

            typedValues.Add(GetParameterTypedValue(criteria, criteriaQuery));

            return typedValues.ToArray();
        }

        private new TypedValue GetParameterTypedValue(ICriteria criteria, ICriteriaQuery criteriaQuery)
        {
            var matchValue = _value.ToUpper(GridModelMapper.ConfigurationObject.DefaultCulture);
            return CriterionUtil.GetTypedValues(criteriaQuery, criteria, _projection, null, matchValue).Single();
        }
    }
}